namespace Strangelights.AntColony

open Microsoft.FSharp.Control.CommonExtensions
open Microsoft.FSharp.Compiler.CodeDom
open System
open System.IO
open System.Drawing
open System.Windows.Forms
open System.Collections.Generic
open System.Diagnostics
open System.Reflection
open ICSharpCode.TextEditor
open ICSharpCode.TextEditor.Document

module Counters =
    /// Short had function for creating counters installation information
    let counterCreationData name ctype = new CounterCreationData(name, "", ctype)

    /// Definitions of our counters installation information
    let antCounters = 
        [| counterCreationData "Frames per second" PerformanceCounterType.RateOfCountsPerSecond64;
           counterCreationData "Messages per second" PerformanceCounterType.RateOfCountsPerSecond64;
           counterCreationData "Ants" PerformanceCounterType.NumberOfItems64|]

    /// Install our counters
    let installCounters() =
        if PerformanceCounterCategory.Exists("Ant Colony Stats") then
            PerformanceCounterCategory.Delete("Ant Colony Stats")
        PerformanceCounterCategory.Create("Ant Colony Stats", 
                                          "", 
                                          PerformanceCounterCategoryType.MultiInstance, 
                                          new CounterCreationDataCollection(antCounters))  
        |> ignore                 

    // Install the counters if necessary
    if Sys.argv.Length > 1 && Sys.argv.[1] = "--install-counters" then
        installCounters()

    /// Create an instance of the counter to be used, returning None if the counter 
    /// is not available because it has not been installed or otherwise
    let createCounter name = 
        try 
            let curProc = Process.GetCurrentProcess()
            Some(new PerformanceCounter("Ant Colony Stats", name, curProc.Id.ToString(), false)) 
        with ex -> 
            printfn "Warning: failed to initalize counter \"%s\", the exception was: %s" name ex.Message
            None

    /// Instances of the counters
    let framesCount = createCounter "Frames per second"
    let messagesCounter = createCounter "Messages per second"
    let antsCounter = createCounter "Ants"

    /// Fire the counters or not depending on whether they exist
    let fireCounter (counter:option<PerformanceCounter>) = 
        match counter with 
        | Some x -> x.Increment() |> ignore
        | _ -> ()


    /// Fire the counters
    let fireFrames() = fireCounter framesCount
    let fireMessages() = fireCounter messagesCounter
    let fireAnts() = fireCounter antsCounter

    /// Remove the counter depending on whether it exists
    let removeInstance (counter:option<PerformanceCounter>) = 
        match counter with 
        | Some x -> x.RemoveInstance()
        | _ -> ()

    // remove the counters when the domain unloads (when the process exits)
    AppDomain.CurrentDomain.DomainUnload.Add(fun _ -> 
        removeInstance framesCount
        removeInstance messagesCounter
        removeInstance antsCounter)
        



type Updates =
    | AntMoved of ((int*int)*(int*int))
    | FoodChanged of ((int*int)*int)

type MovementCollector() = 
    // Capture the synchronization context of the thread that creates this object. This
    // allows us to send messages back to the GUI thread painlessly.
    let callerCtxt = 
        match System.Threading.SynchronizationContext.Current with 
        | null -> null // System.ComponentModel.AsyncOperationManager.SynchronizationContext
        | x -> x
    //do if callerCtxt = null then failwith "Couldn't detect the synchronization context of the calling thread"
        
    let runInGuiCtxt f = 
        match callerCtxt with 
        | null -> 
            // callerCtxt is null on Mono. This is a bug. System.Threading.SynchronizationContext.Current doesn't return a useful
            // result. This is a little unfortunate. System.ComponentModel.AsyncOperationManager.SynchronizationContext returns
            // an inconsistent result.
            //
            // So here we works around, where we finds the open form and sends to it. 
            if System.Windows.Forms.Application.OpenForms.Count > 0 then 
                System.Windows.Forms.Application.OpenForms.Item(0).BeginInvoke(new System.Windows.Forms.MethodInvoker(fun _ -> f())) |> ignore
        | _ -> callerCtxt.Post((fun _ -> f()),null)

    // This events are fired in the synchronization context of the GUI (i.e. the thread
    // that created this object)
    let fireUpdates,onUpdates = Event.create() 

    let mailboxProcessor = 
        new MailboxProcessor<Updates>(fun inbox -> 
            /// This is the States of the worker's automata using a set of 
            /// tail-calling recursive functions. 
            let rec loop lastUpdate updates = 
                async { let! msg = inbox.Receive()
                        let updates = msg :: updates 
                        if DateTime.Now - lastUpdate > new TimeSpan(0,0,0,0,50) then
                            do Counters.fireFrames()
                            do runInGuiCtxt(fun () -> fireUpdates(updates))
                            return! loop DateTime.Now [] 
                        return! loop lastUpdate updates }
            loop DateTime.Now [])
            
    
    /// Here is the public API to the worker
    member w.AntMoved (oldLoc, newLoc) = mailboxProcessor.Post(AntMoved(oldLoc, newLoc))
    member w.FoodChanged(cell, value) = mailboxProcessor.Post(FoodChanged(cell, value))
    member w.Updates       = onUpdates
    member w.Start()  = mailboxProcessor.Start()


/// the direction that the ants travel in    
type Direction = North | East | South | West

/// represents an ant that moves within the territory                      
type Ant = 
    { id: int
      foodCarried: int }
  with
    /// creates an new instance of an ant with the default values
    static member newAnt id =
      { id = id; foodCarried = 0; }


/// represents a single node in the ants territory
type TerritoryNode = 
    { food: int;
      pheromone: int;
      isHome: bool
      ant: option<Ant> }
  with
    /// creates a new node with the default values
    static member newNode home =
        { food = 0;
          pheromone = 0;
          isHome = home;
          ant = None }
    /// creates a new node with food
    static member newNodeFood f home =
        { food = f;
          pheromone = 0;
          isHome = home;
          ant = None; }

type TerritoryView =
    { foodLevel: int;
      pheromoneLevel: int;
      isAntHome: bool;
      hasAnt: bool }
    with 
        static member fromTerritoryNode n =
            { foodLevel = n.food;
              pheromoneLevel = n.pheromone;
              isAntHome = n.isHome;
              hasAnt = match n.ant with | None -> false | _ -> true; }
        static member fromOptionTerritoryNode n =
            match n with 
            | Some n -> Some (TerritoryView.fromTerritoryNode n)
            | _ -> None

type Surroundings =
    { Current: TerritoryView;
      North: Option<TerritoryView>;
      East: Option<TerritoryView>; 
      South: Option<TerritoryView>;
      West: Option<TerritoryView>; }

type IAntBehavior =
    abstract Behave: int*Surroundings -> Direction*int*int


type NodeCommands =
    | RequestValues of int*AsyncReplyChannel<int*Surroundings> 
    | RequestMove of int*Direction*int*int
    | Stop

type NodesManager(mc:MovementCollector, territoryWidth:int, territoryHeight:int, antHome:int, 
                  chanceFoodPerSquare:int, maxFoodPerSquare:int, antMaxFoodCarried:int) = 
    let rand = new System.Random()
    
    /// test that a node is within the grid
    let isNodeReal x y = 
        0 <= x && x < territoryWidth && 
        0 <= y && y < territoryHeight
    
    let nodeMap =
        //create a list of points to represent the grid
        let points = 
            seq { for i in [0 .. territoryWidth - 1] do 
                    for j in [0 .. territoryHeight - 1] do
                      yield i,j }
                      
        // randomly creat a node
        let createNode i j =
           let randomFoodDropValue = rand.Next(0, chanceFoodPerSquare) in
           let home = i <= antHome && j <= antHome
           if randomFoodDropValue = 5 then
               let food = rand.Next(0, maxFoodPerSquare)
               mc.FoodChanged((i,j), food)
               TerritoryNode.newNodeFood food home
           else 
               TerritoryNode.newNodeFood 0 home
        
        // create a map of points to node values
        Seq.fold (fun acc (i,j) -> Map.add (i,j) (createNode i j) acc) Map.empty points
    
    let replaceNode (nodeMap : Map<_,_>) loc newNode = 
        if nodeMap.ContainsKey(loc) then
            let nodeMap = nodeMap.Remove(loc)
            nodeMap.Add(loc, newNode)
        else
            nodeMap.Add(loc, newNode)
            
    let ants =
        [ for x in [0 .. antHome] do
            for y in [0 .. antHome] do
                do mc.AntMoved((territoryWidth - 1, territoryHeight - 1),(x,y))
                yield x, y, Ant.newAnt (x + (y * antHome))]
                
    let nodeMap = List.fold_left (fun acc (x,y,ant) -> replaceNode acc (x,y) { acc.[(x,y)] with ant = Some ant }) nodeMap ants
    let antLocMap = Seq.fold (fun acc (x,y,ant) -> Map.add (ant.id) (x,y) acc) Map.empty ants
        
    let nodeMailbox = MailboxProcessor.Start(fun inbox ->
        // The states of the message processing state machine...
        let rec loop((nodeMap: Map<int*int,TerritoryNode>), (antLocMap: Map<int,int*int>)) = 
            let getNode x y = if isNodeReal x y then (Some nodeMap.[(x,y)]) else None
            let mapCoor x y direction =
                match direction with 
                | North -> x, (y - 1)
                | East -> (x + 1), y
                | South -> x, (y + 1)
                | West -> (x - 1), y
            async { let! msg = inbox.Receive()
                    do Counters.fireMessages()
                    match msg with 
                    | RequestMove (antid,direction,foodTake,pheremoneDrop) ->
                        let x,y = antLocMap.[antid]
                        let node = nodeMap.[(x,y)]
                        let ant = match node.ant with | Some x -> x |  _ -> failwith (Printf.sprintf "RequestMove: Ant %i not at %i %i" antid x y)
                        let space = antMaxFoodCarried - ant.foodCarried
                        let antsFood,squaresFood =
                            if foodTake > 0 then 
                                //printfn "taking food ..."
                                let foodTake = min foodTake space
                                if node.food >= foodTake then 
                                    ant.foodCarried + foodTake, node.food - foodTake
                                else 
                                    node.food,0
                            else if foodTake = 0 then
                                0, node.food
                            else
                                //printfn "dropping food ..."
                                let dropableFood = min (-foodTake) (maxFoodPerSquare - node.food)
                                if dropableFood >= ant.foodCarried then
                                    0,ant.foodCarried + node.food
                                else
                                    ant.foodCarried-dropableFood,dropableFood
                        if squaresFood <> 0 then
                            do mc.FoodChanged((x,y),squaresFood)
                        let ant = { ant with foodCarried = antsFood }
                        let node = { node with food = squaresFood }
                        let x', y' = mapCoor x y direction
                        let newNode = getNode x' y'
                        //do printfn "Ant %i RequestMove %i %i -> %i %i" antid x y x' y';
                        match newNode with
                        | Some newNode ->
                            if (match newNode.ant with | None -> true | _ -> false) then
                                do mc.AntMoved((x,y),(x',y'))
                                let node, newNode = { node with ant = None }, { newNode with ant = Some ant } 
                                let nodeMap = replaceNode nodeMap (x,y) node 
                                let nodeMap = replaceNode nodeMap (x',y') newNode 
                                let antLocMap = replaceNode antLocMap antid (x',y')
                                return! loop(nodeMap, antLocMap)
                            else
                                //do printfn "Ant %i Move Failed %i %i -> %i %i" antid x y x' y'
                                let node = { node with ant = Some ant } 
                                let nodeMap = replaceNode nodeMap (x,y) node 
                                return! loop(nodeMap, antLocMap)
                        | _ -> 
                            let node = { node with ant = Some ant }
                            //do printfn "Ant %i Node none existant %i %i -> %i %i" antid x y x' y'
                            return! loop(replaceNode nodeMap (x,y) node,antLocMap)
                    | RequestValues (id,channel)  ->
                        let x,y = antLocMap.[id]
                        let node = nodeMap.[(x,y)]
                        let ant = match node.ant with Some x -> x | _ -> failwith (Printf.sprintf "RequestValues: Ant %i not at %i %i" id x y)
                        //do printfn "Ant %i RequestValues %i %i" id x y;
                        //if node.food > 0 then
                        //    do printfn "%A" node
                        let surroundings = 
                            { Current = TerritoryView.fromTerritoryNode node;
                              North = TerritoryView.fromOptionTerritoryNode (getNode (x + 1) y);
                              East = TerritoryView.fromOptionTerritoryNode (getNode x (y + 1)); 
                              South = TerritoryView.fromOptionTerritoryNode (getNode (x - 1) y);
                              West = TerritoryView.fromOptionTerritoryNode (getNode x (y - 1)); }
                        do channel.Reply((ant.foodCarried,surroundings)) 
                        return! loop(nodeMap, antLocMap) 
                    | Stop -> 
                        // exit
                        return () } 

        // The initial state of the message processing state machine...
        loop(nodeMap, antLocMap))
    /// Here is the public API to the worker
    member w.RequestMove(ant,direction,food,pher) = nodeMailbox.Post(RequestMove(ant,direction,food,pher))
    member w.RequestValues(id) = nodeMailbox.AsyncPostAndReply(fun rc -> RequestValues(id,rc))
    member w.Stop() = nodeMailbox.Post(Stop)


module Program =
    //Program constants
    let antHome = 10 //X by X in Size
    let maxFoodPerSquare = 1000
    let chanceFoodPerSquare = 80

    let antMaxFoodCarried = 50

    let territoryHeight = 100
    let territoryWidth = 100

    let mutable behavior = None

    let behaviorCode = @"
open Strangelights.AntColony

let south = { new IAntBehavior with member x.Behave (_, _) = Direction.South, 0, 0 }
let north = { new IAntBehavior with member x.Behave (_, _) = Direction.North, 0, 0 }
let east = { new IAntBehavior with member x.Behave (_, _) = Direction.East, 0, 0 }
let west = { new IAntBehavior with member x.Behave (_, _) = Direction.West, 0, 0 }

let rand = new System.Random()

let hunt food =
    let num = rand.Next(2)
    //if food > 0 then 
    //    printfn ""Food: %i Number: %i"" food num
    if food > 0 then
        match num with
        | 0 -> Direction.North
        | 1 -> Direction.West
        | _ -> failwith ""assert false""
    else
        match num with
        | 0 -> Direction.East
        | 1 -> Direction.South 
        | _ -> failwith ""assert false""

let huntFood = { new IAntBehavior with member x.Behave (food, t) = hunt food, (if t.Current.isAntHome then -50 else 50), 0 }"

    let main() =
        // definition of controls
        let bitmap = new Bitmap(territoryWidth, territoryHeight, System.Drawing.Imaging.PixelFormat.Format24bppRgb)
        let form = new Form(Visible=true,Text="Ant Colony",Width=800, Height=700)
        let tabcontrol = new TabControl(Dock=DockStyle.Fill)
        let antsTab = new TabPage("Ants")
        tabcontrol.TabPages.Add(antsTab)
        let codeTab = new TabPage("Code")
        tabcontrol.TabPages.Add(codeTab)
      
        let pb = new PictureBox(Width=600, Height=600,
                                SizeMode=PictureBoxSizeMode.Zoom)
        pb.Image <- bitmap
        let behaviorsLabel = new Label(Left=600, Top=40, Width=60, Text="Behaviors: ")
        let behaviors = new ComboBox(Left=660, Top=40, DropDownStyle=ComboBoxStyle.DropDownList)  
        let antsLabel = new Label(Left=600, Top=100, Text="Ants: ")
        let ants = new Label(Left=700, Top=100, Text=(antHome * antHome).ToString())
        let framesPerSecLabel = new Label(Left=600, Top=130, Text = "Frames p/s:")
        let framesPerSec = new Label(Left=700, Top=130)
        let messagesPerSecLabel = new Label(Left=600, Top=160, Text = "Messages p/s:")
        let messagesPerSec = new Label(Left=700, Top=160)
        let threadsLabel = new Label(Left=600, Top=190, Text="Threads:")
        let threads = new Label(Left=700, Top=190)
      
        let dc c = c :> Control
        antsTab.Controls.AddRange([|dc behaviorsLabel; dc behaviors; dc antsLabel; dc ants; 
                                    dc framesPerSecLabel; dc framesPerSec; dc messagesPerSecLabel; dc messagesPerSec;
                                    dc threadsLabel; dc threads; dc pb; |])
                                  
        let splitCont = new SplitContainer(Orientation = Orientation.Horizontal, Dock = DockStyle.Fill)
        let editor = new TextEditorControl(Dock = DockStyle.Fill, Text = behaviorCode, FileName = "behaviors.fs")
        let fileSyntaxProvider = new FileSyntaxModeProvider(".")
        HighlightingManager.Manager.AddSyntaxModeFileProvider(fileSyntaxProvider)
        editor.SetHighlighting("F#")
        if File.Exists(editor.FileName) then
            editor.LoadFile(editor.FileName)
            
        let errors = new ListBox(Dock = DockStyle.Fill)
      
        let currentBehaviors = new Dictionary<string, IAntBehavior>()
      
        let build() =
            currentBehaviors.Clear()
            behaviors.Items.Clear()
            errors.Items.Clear()
            behavior <- None
            
            let codeDom = new  FSharpCodeProvider()
            let file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AntColony.exe")
            let parameters = new System.CodeDom.Compiler.CompilerParameters([|file|])
            parameters.CompilerOptions <- "--nologo"
            let res = codeDom.CompileAssemblyFromSource(parameters, [|editor.Text|])
            for err in res.Errors do
                errors.Items.Add(Printf.sprintf "%s %s(%i): %s" (if err.IsWarning then "Warning" else "Error")  err.FileName err.Line err.ErrorText) |> ignore
            let errs = seq { for err in res.Errors -> err } |> Seq.filter (fun err -> not err.IsWarning) 
            if Seq.length errs = 0 then
                let behaviorFields =
                    res.CompiledAssembly.GetTypes()
                    |> Array.map (fun t -> List.of_array (t.GetProperties(BindingFlags.Public ||| BindingFlags.Static)))
                    |> List.of_array
                    |> List.concat
                    |> List.filter (fun f -> f.PropertyType.IsAssignableFrom(typeof<IAntBehavior>))

                for f in behaviorFields do
                    let be = f.GetValue(null, [||]) :?> IAntBehavior
                    currentBehaviors.Add(f.Name, be)
                    behaviors.Items.Add(f.Name) |> ignore
                behaviors.SelectedIndex <- 0
        let openFile() =
            let openFileDiaglog = new OpenFileDialog()
            if openFileDiaglog.ShowDialog() = DialogResult.OK then
                editor.LoadFile(openFileDiaglog.FileName)
        let saveAs() = 
            let saveFileDiaglog = new SaveFileDialog()
            if saveFileDiaglog.ShowDialog() = DialogResult.OK then
                editor.SaveFile(saveFileDiaglog.FileName)
        let save = new MenuItem("Save", (fun _ _ -> editor.SaveFile(editor.FileName)), Shortcut.CtrlS)
        let saveAs = new MenuItem("Save as ...", (fun _ _ -> saveAs()))
        let openFile = new MenuItem("Open", (fun _ _ -> openFile()), Shortcut.CtrlO)
        let file = new MenuItem("File", [|openFile; save; saveAs; |])
        let bb = new MenuItem("Build Behaviors", (fun _ _ -> build()), Shortcut.CtrlShiftB)
        let buildMenu = new MenuItem("Build", [|bb|])
        let menu = new MainMenu([|file; buildMenu|])
        form.Menu <- menu
      
        behaviors.SelectedIndexChanged.Add(fun _ ->
            if behaviors.SelectedIndex >= 0 then 
                let beName = behaviors.Items.[behaviors.SelectedIndex].ToString()
                behavior <- Some(currentBehaviors.[beName]))
                
        let t = new Timer(Interval=1000)
        let curProcess = Process.GetCurrentProcess()
        t.Tick.Add(fun _ -> 
            framesPerSec.Text <- Counters.framesCount.Value.NextValue().ToString()
            messagesPerSec.Text <- Counters.messagesCounter.Value.NextValue().ToString()
            threads.Text <- curProcess.Threads.Count.ToString())
        t.Start()
      
        build()            
      
        splitCont.Panel1.Controls.Add(editor)      
        splitCont.Panel2.Controls.Add(errors)      
        codeTab.Controls.Add(splitCont)
        form.Controls.Add(tabcontrol)
        form.Closing.Add(fun _ -> editor.SaveFile(editor.FileName))


        let mc = new MovementCollector()

        let nodesManager = new NodesManager(mc, territoryWidth, territoryHeight, antHome, 
                                          chanceFoodPerSquare, maxFoodPerSquare, antMaxFoodCarried)
                                        
        let rec antWorkflowLoop id = 
            async { let! food,surroundins = nodesManager.RequestValues(id)
                    do
                        match behavior with
                        | Some (be: IAntBehavior) ->
                            let direction,food,pher = be.Behave(food,surroundins)
                            nodesManager.RequestMove(id,direction,food,pher)
                        | _ -> ()
                    return! antWorkflowLoop id }
                    
        let ants =
            [ for x in [0 .. antHome] do
                for y in [0 .. antHome] do
                    do mc.AntMoved((territoryWidth - 1, territoryHeight - 1),(x,y))
                    yield (x + (y * antHome))]
                    
        let antWorkFlows = ants |> Seq.map (fun id -> antWorkflowLoop id) 
        
        for wf in antWorkFlows do
            Async.Spawn(wf)

        mc.Updates.Add(fun updates ->
            let updates = List.rev updates
            for  update in updates do
                match update with
                | AntMoved((oldx,oldy),(newx,newy)) ->
                    //printfn "old %i %i new %i %i" oldx oldy newx newy
                    bitmap.SetPixel(oldx,oldy, Color.Black)
                    bitmap.SetPixel(newx,newy, Color.Red)
                | FoodChanged((x,y),food) ->
                    let c = Color.FromArgb( food / maxFoodPerSquare * 255, Color.Green)
                    bitmap.SetPixel(x,y, c)
            pb.Invalidate() 
            //System.Diagnostics.Debugger.Break()
            )
          
        form.Visible <- true
        form.Activate()
        mc.Start()
        
        Application.Run(form)

    [<STAThread>]
    do main()
